-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better Overload Support #93
base: master
Are you sure you want to change the base?
Better Overload Support #93
Conversation
The detailed design section is supposed to be detailed. It's fine to have an incomplete starting point and work open questions out with the community, but for something as complicated as full overload support this is way too little. I'll leave it open for now, but in this state it's not even worth discussing much. Some starting points:
I've always been against this feature because in my estimation it comes with too many additional requirements for the run-time. |
You can just specify that it is exactly the same as properties.
|
I do think it should be compile time and that was what I was intending. I don't even know how it would happen on runtime. |
Now that I think about, maybe it's unwise to not have a "true" function. I'd say maybe we could make a function that uses optional parameters but that isn't fully cross compatible afaik. |
How would variance affect things? Simply doing how it already works, by "downgrading" (taking a subclass and removing all its properties that make it different from its parent) should work. |
The main problem is diamond inheritance via interfaces. interface I1 {
function method(a:A):Void;
}
interface I2 {
function method(b:B):Void;
}
class X implements I1 implements I2 {}
|
Are you even allowed to have an two interfaces with functions of the same name? That seems silly. |
Well, a compile-time-only feature is a long way from the proposed "fully work between all languages". I can't say that it sounds very attractive considering the complications it comes with and the inconsistencies that will arise. You will have to decide to what extent you want to support dynamic dispatch. It might be feasible to simply disallow some things here, but then the proposal has to say so. Basically, if you want to support |
This is all confusing, this is my first proposal so bear with me. What I want is java like overloading support, for all target langs, either by using their built in overloading or emulating it. This proposal is more concerned with emulation. Fully work with all languages simply means it can compile to all languages. For languages that don't natively support overloading, like hashlink, it's emulated. This proposal says that overloaded functions are renamed based on their type signature, and how haxe determines what signature is wanted. I think as much as possible should be supported, but I'm not very good at working this stuff out. All that was in my mind making this was classes. Interfaces probably should not support overloading. |
Here's an example to make this easier to follow: class Parent<T> {
public function new() { }
public function test(t:T) {
trace("Parent.test(T)");
}
}
class Child extends Parent<String> {
public override overload function test(s:String) {
trace("Child.test(String)");
}
}
function main() {
var child = new Child();
child.test("foo");
var parent:Parent<String> = child;
parent.test("foo");
var dyn:Dynamic = child;
dyn.test("foo");
}
Note that all three of these work on targets with real overload support. |
The third one is impossible to error at compile time because it's dynamic correct? If so, then failing at runtime is probably the only solution, or using the |
Dynamic dispatch is the thing that causes the child class function to be called even though the call is made on a reference typed as the parent class. If you think about it you will realize that this cannot be done at compile-time, hence dynamic dispatch. Regarding adapter functions: Try constructing how the second example looks with overload functions being renamed (in whichever way). I think you'll naturally end up with an adapter function to make it work. |
Another option might be to come at it from a different direction - that is call the "pseudo constructor" (static function that returns an instance) whatever you like and add the meta ':op(new)' or similar. function new(a:Int, b:Float) { ... }
@:op(new) public static function create(x:Int) return new Test(x*3, x*5);
var test = new Test(1); // Generates Test.create(1); Making new private here might also help clear up ambiguous matching. But compare Fore me, the more general ability to overload 'create' is more interesting. Restricting to static functions might be a way of dodging dynamic resolution issues initially. |
Please note that this already works, relying on the class Test {
function new(a:Int, b:Float) { }
public overload extern inline static function create(x:Int) return new Test(x * 3, x * 5);
public overload extern inline static function create(x:Int, y:Int) return new Test(x * 3, y * 5);
}
function main() {
Test.create(1);
Test.create(1, 2);
} The only problem is that you need 5 modifiers... I'm not strictly against extending this to non-inline |
Hey, I did not know that! As you say, the "extern" seems not only redundant but contradictory given that you have a function body, and I assume there is no technical/practical requirement for inline for a static function since dynamic dispatch confusion is not possible? Seems like an unnecessary restriction - but I guess you could alway just inline a call to a non-inline function if you really wanted. |
Actually, If we go the renaming route here to actually generate these overloads, there would be increased cognitive burden because it adds more exceptions to which functions aren't callable at run-time. That's not a hard showstopper, but it does make me reluctant to extend support for this in some half-assed way... |
So would the adapter function be the "correct" name? // from child extending Parent<String>
override overload function test(i:String)
// from parent
function test<T>(i:T) would become // child
override function test(i:Dynamic) {
// switch runtime type
switch (Type.typeof(i)) {
// Or whatever the proper switch is. Since at compile time String == String this is the only case
case TClass(String):
test_string(i);
// if it wasn't same at compile time
// case TClass(String):
// super.test(i);
}
}
function test_string(i:String)
// from parent. we know however that T is string so this isn't used for dynamic dispatch
function test(i:T) |
I don't like the idea of runtime type switching though. |
It probably would make sense to always keep an adapter function on every class to support dynamic dispatch. However it would still probably be optimal to rename any function we know uses a specific overload (because that type switching is probably less performant than simply calling a function). This also would mess up inlining really badly. How the heck do you inline that switch case? |
Such a function would likely be needed for the full dynamic route, yes. But that's actually one step ahead of the other problem (case 2 from my example). In that situation, Another question will be how to design the naming function with regards to type parameters. |
I think we should ditch the lowercasing. That just makes it easier to make mistakes, and it allows stuff like overload function test(i:TestClass)
overload function test(i:Testclass) to become function test_testclass(i:TestClass)
function test_testclass(i:Testclass) which is bad. I also think we shouldn't add the return type if it doesn't matter (I stole the idea of it not mattering from java too.) I also just realized something. Without a fully qualified typepath names could be the same. overload function test(i:bulby.Vector3)
overload function test(i:peote.Vector3) wouldn't work: function test_Vector3(i:bulby.Vector3)
function test_Vector3(i:peote.Vector3) It would be silly to put full type paths tho. For byte code targets (like hashlink and neko) we could just get away with assigning a unique identifier and calling it a day. For targets that must be readable then we could get away with only qualifying if there would be a conflict otherwise. Based off that earlier example: function test_bulby_Vector3(i:bulby.Vector3)
function test_peote_Vector3(i:peote.Vector3) I can see why this needs further explaining. Why would it need to override |
That makes me think, how would overriding an overloaded function work? class Parent {
overload function test(i:String)
overload function test(i:Int)
}
class Child extends Parent {
override overload function test(i:Vector3)
override overload function test(i:String)
} would become class Parent {
function test(i:Dynamic) {
switch (Type.typeof(i)) {
case TClass(String):
test_String(i);
case TInt:
test_Int(i);
}
}
function test_String(i:String)
function test_Int(i:Int)
}
class Child extends Parent {
override function test(i:Dynamic) {
switch (Type.typeof(i)) {
case TClass(Vector3):
test_Vector3(i);
default:
super.test(i);
}
}
override function test_String(i:String)
} |
Right, adding |
Yes, I just made a case for this. |
I feel like technically this could be done with macros but it'd be a really janky, messy code solution. |
I'll update the proposal tommorow to cover these edge cases. |
Ok I'm updating it, but you know how constructors return nothing? How would I make an adapter function for that? |
I've made the decision to rename the overload converted functions with a |
I'm not sure what's unclear here. The following compiles and runs with the expected output on java, jvm and cs targets: function main() {
var x = new X(),
a = new A(),
b = new B();
(x:I1).method(a);
(x:I2).method(b);
x.method(a);
x.method(b);
}
class A {
public function new() {}
}
class B {
public function new() {}
}
interface I1 {
function method(a:A):Void;
}
interface I2 {
function method(b:B):Void;
}
class X implements I1 implements I2 {
public function new() {}
overload public function method(a:A) { trace(a); }
overload public function method(b:B) { trace(b); }
} If you're intending to not support this case, then that may be a wise decision, but it still deserves mention (because "full" overload support suggests otherwise) and finally needs a good error message / documentation. That said, I'm leaning towards believing that that could be handled by the "adapter method" that dispatches on type. I suspect though that on static platforms without overloads it means that the runtime type of |
What I'm really worried about is if there are two functions overloaded together and one of them is void and another is some other random type |
I'm also positive we could implement this, but I would have to figure out a way to write that into a proposal. |
I just noticed that haxe already knows how to mangle names; it does it with the |
Adds full overload support by renaming functions and function calls when compiling
Rendered Proposal